#include <string.h>

#include "metainfo_ape.h"
#include "metainfo_mp3.h"

struct ape_header{
  gchar preamble[8];
  int version;
  int size;
  int count;
  int flag;
  gchar reserved[8];
};

struct ape_item{
  int len;
  int flag;
  gchar key_value[0];
};

struct list_item{
  struct ape_item *item;
  void *data;
};

struct ape_tag{
  struct ape_header header;
  struct ape_header footer;
  GList *items;
  gchar *path;
  FILE *fp;
  gboolean modified;
  gboolean atend;
  void *data;
};


static gboolean __ape_tag_detect(FILE *fp, gboolean *atend, gboolean *haveid3)
{
  struct ape_header header;
  ID3V1 *id3;

  id3 = check_id3v1(fp);
  if(id3)  {
	if(haveid3)
	  *haveid3 = TRUE;
	fseek(fp, - (sizeof(struct ape_header) + sizeof(ID3V1)), SEEK_END);
	g_free(id3);
  }else  {
	if(haveid3)
	  haveid3 = FALSE;
	fseek(fp, - sizeof(struct ape_header), SEEK_END);
  }
  fread(&header, 1, sizeof(struct ape_header), fp);
  if(! strncmp(header.preamble, "APETAGEX", 8))  {
	if(atend)  *atend = TRUE;
	return TRUE;
  }
  memset(&header, 0, 8);
  rewind(fp);
  fread(&header, 1, sizeof(struct ape_header), fp);
  if(! strncmp(header.preamble, "APETAGEX", 8))   {
	if(atend)  *atend = FALSE;
	return TRUE;
  }

  return FALSE;
}


gboolean         ape_tag_detect(const gchar *path, gboolean *atend, gboolean *haveid3)
{
  FILE *fp;
  gboolean ret;

  if(! path)
	return FALSE;
  fp = fopen(path, "r");
  if(! fp)
	return FALSE;
  ret = __ape_tag_detect(fp, atend, haveid3);
  fclose(fp);

  return ret;
}


struct ape_tag  *ape_tag_new(gint version)
{
  struct ape_tag *tag;

  if(version < 2000)
	version = 1000;
  else
	version = 2000;

  tag = g_new0(struct ape_tag, 1);
  strncpy(tag->header.preamble, "APETAGEX", 8);
  tag->header.version = version;
  tag->header.flag = 0xa0000000;
  tag->header.size = sizeof(struct ape_header);
  memcpy(&tag->footer, &tag->header, sizeof(struct ape_header));
  tag->footer.flag = 0x80000000;

  return tag;
}

gboolean __ape_tag_load_items(struct ape_tag *tag)
{
  gint i, j = 0;
  struct list_item *li;

  tag->data = g_new(gchar, tag->header.size);
  do{
	i = fread(tag->data + j, 1, tag->header.size - j, tag->fp);
	j += i;
  }while(j < tag->header.size && ! feof(tag->fp));
  if(j < tag->header.size)  {
	DBGMSG("corrupted tag info, abort tag items parsig.\n");
	return FALSE;
  }

  for(i = 0, j = 0; j < tag->header.size && i < tag->header.count;)  {
	li = g_new(struct list_item, 1);
	li->data = NULL;
	li->item = (struct ape_item *)(tag->data + j);
	j += sizeof(struct ape_item) + strlen(li->item->key_value);
	j += li->item->len + 1;
	i++;
	tag->items = g_list_prepend(tag->items, li);
  }

  if(j <= tag->header.size)  {
	DBGMSG("corrupted tag: missing footer.\n");
	return FALSE;
  }

  if(j + sizeof(struct ape_header) != tag->header.size)  {
	DBGMSG("size mimatch.\n");
	return FALSE;
  }

  memcpy(&tag->footer, tag->data + j, sizeof(struct ape_header));

  return TRUE;
}


struct ape_tag  *__ape_tag_load(FILE *fp, const gchar *path)
{
  gboolean atend, haveid3;
  struct ape_tag *tag;
  gint extra = 0;

  if(! __ape_tag_detect(fp, &atend, &haveid3))
	return NULL;
  tag = g_new0(struct ape_tag, 1);
  tag->path = g_strdup(path);
  tag->fp = fp;
  tag->atend = atend;
  if(atend)  {
	if(haveid3)
	  extra = sizeof(ID3V1);
	fseek(fp, - (sizeof(struct ape_header) + extra), SEEK_END);
	fread(&tag->footer, 1, sizeof(struct ape_header), fp);
	if(tag->footer.version == 2000)  {
	  fseek(fp, -(sizeof(struct ape_header) + tag->footer.size + extra), SEEK_END);
	  fread(&tag->header, 1, sizeof(struct ape_header), fp);
	}else  {
	  fseek(fp, -(tag->footer.size + extra), SEEK_END);
	  memcpy(&tag->header, &tag->footer, sizeof(struct ape_header));
	  tag->header.flag = 0xa0000000;
	}
	__ape_tag_load_items(tag);
  }else{
	rewind(fp);
	fread(&tag->header, 1, sizeof(struct ape_header), fp);
	__ape_tag_load_items(tag);
  }

  return tag;
}


struct ape_tag  *ape_tag_load(const gchar *path)
{
  FILE *fp;

  if(! path)  return NULL;
  fp = fopen(path, "r");
  if(! fp)  return NULL;
  return __ape_tag_load(fp, path);
}



static gboolean   __ape_tag_save(struct ape_tag *tag, FILE *fp)
{
  gboolean atend, haveid3;
  gint  offset = 0;
  struct ape_header footer;
  struct list_item *item;
  GList *iter;

  if(__ape_tag_detect(fp, &atend, &haveid3))  {
	  if(haveid3)
		offset -= sizeof(ID3V1);
	if(atend)  {
	  fseek(fp, - sizeof(struct ape_header), SEEK_END);
	  if(fread(&footer, 1, sizeof(struct ape_header), fp) == sizeof(struct ape_header))  {
		offset -= footer.size;
		if(footer.version == 2000)
		  offset -= sizeof(struct ape_header);
	  }
	}
	fseek(fp, offset, SEEK_END);
  }else
	fseek(fp, 0, SEEK_END);

  if(tag->header.version == 2000)
	fwrite(&tag->header, 1, sizeof(struct ape_header), fp);
  iter = tag->items;
  if(iter)
	do{
	  item = (struct list_item *)iter->data;
	  fwrite(item->item, 1, sizeof(struct ape_item), fp);
	  fwrite(item->item->key_value, 1, item->item->len, fp);
	}while(iter = g_list_next(iter));
  fwrite(&tag->footer, 1, sizeof(struct ape_header), fp);
  ftruncate(fileno(fp), ftell(fp));

  return TRUE;
}


gboolean         ape_tag_save(struct ape_tag *tag, const gchar *path)
{
  FILE *fp;
  gboolean ret;

  if(! tag)
	return FALSE;
  if(path)  {
	fp = fopen(path, "rw");
	if(! fp)
	  return FALSE;
  }else  {
	if(tag->fp)  {
	  fclose(tag->fp);
	  tag->fp = NULL;
	}
	fp = fopen(tag->path, "rw");
	if(! fp)
	  return FALSE;
  }

  ret =__ape_tag_save(tag, fp);
  fclose(fp);

  return ret;
}

const gchar     *ape_tag_read_tag(struct ape_tag *tag, const gchar *key, gint *len)
{
  GList *tmp = tag->items;
  struct list_item *li;
  const gchar *value;

  if(! tag || ! key || ! tag->items)
	return NULL;

  do{
	li = (struct list_item *)tmp->data;
	if(! strcasecmp(li->item->key_value, key))
	  break;
  }while(tmp = g_list_next(tmp));

  if(! tmp)  return NULL;
  value = li->item->key_value;
  while(*value++);
  if(len)
	*len = li->item->len;

  return value;
}



gboolean         ape_tag_write_tag(struct ape_tag *tag, const gchar *key, const gchar *value, gint len)
{
  struct list_item *li;
  GList *list;
  gchar *buf, *tmp;
  gint total_len = 0;

  if(! tag || ! key || ! value)
	return FALSE;
  if(len == 0 || (len == -1  && ! *value) || len < -1)
	return FALSE;

  list = tag->items;
  while(list)  {
	li = (struct list_item *)list->data;
	if(! strcasecmp(li->item->key_value, key))
	  break;
	list = g_list_next(list);
  }

  tag->modified = TRUE;
  if(len == -1)
	len = strlen(value);
  total_len += sizeof(struct ape_item) + strlen(key) + len + 1;
  if(! list)  {
	li = g_new(struct list_item, 1);
	tag->header.count++;
	tag->footer.count++;
	tag->header.size += total_len;
	tag->footer.size += total_len;
	li->data = NULL;
	tag->items = g_list_prepend(tag->items, li);
  }else  {
	tag->header.size -= li->item->len;
	tag->footer.size -= li->item->len;
	tag->header.size += len;
	tag->footer.size += len;
  }

  buf = g_new(gchar, total_len);
  li->item = (struct ape_item *)buf;
  g_free(li->data);
  li->data = buf;
  li->item->len = len;
  li->item->flag = 0;
  tmp = li->item->key_value;
  for(;*key; *tmp++ = *key++);
  *tmp = '\0';
  memcpy(tmp, value, len);

  return TRUE;
}


void __free_elem(gpointer data, gpointer udata)
{
  struct list_item *li = (struct list_item *)data;

  g_free(li->data);
  g_free(li);
}


void ape_tag_free(struct ape_tag *tag)
{
  if(! tag)  return;
  g_free(tag->path);
  if(tag->fp)
	fclose(tag->fp);
  g_free(tag->data);
  g_list_foreach(tag->items, __free_elem, NULL);
  g_list_free(tag->items);
  g_free(tag);
}

#ifndef NDEBUG

static void dump_ape_tag(struct ape_tag *tag)
{
  GList *iter;
  struct list_item *item;
  const gchar *p;

  printf("APE TAG DUMP\n");
  puts("==========================================\n"); 

  printf("SIZE: %d  VERSION: %d  COUNT: %d\n",
		 tag->footer.size, tag->footer.version, tag->footer.count);
  iter = tag->items;
  while(iter)  {
	item = (struct list_item *)iter->data;
	printf("%s ---->", item->item->key_value);
	p = item->item->key_value;
	for(;*p++;);
	printf("%.*s\n", item->item->len - strlen(item->item->key_value) -1, p);
	iter = g_list_next(iter);
  }

  puts("==========================================\n"); 
}


#endif

gboolean ape_probe(FILE *fp)
{
  return __ape_tag_detect(fp, NULL, NULL);
}


gboolean ape_read(FILE *fp, struct meta *meta)
{
  struct ape_tag *tag;
  const gchar *value;
  gint len;

  tag = __ape_tag_load(fp, NULL);
  if(! tag)
	return FALSE;

  value = ape_tag_read_tag(tag, "artist", &len);
  meta->artist = g_strndup(value, len);
  value = ape_tag_read_tag(tag, "title", &len);
  meta->title = g_strndup(value, len);
  value = ape_tag_read_tag(tag, "title", &len);
  meta->title = g_strndup(value, len);
  value = ape_tag_read_tag(tag, "album", &len);
  meta->album = g_strndup(value, len);
  value = ape_tag_read_tag(tag, "year", &len);
  meta->year = g_strndup(value, len);
  value = ape_tag_read_tag(tag, "track", &len);
  meta->track = g_strndup(value, len);
  value = ape_tag_read_tag(tag, "genre", &len);
  meta->genre = g_strndup(value, len);
  value = ape_tag_read_tag(tag, "comment", &len);
  meta->comment = g_strndup(value, len);

  meta->internal_encoding = "utf-8";
  meta->lang = "zh_CN";
  meta->sync = FALSE;
  meta->iserr = FALSE;
  meta->errstr = NULL;
  meta->data = tag;

  return TRUE;
}


gboolean ape_write(FILE *fp, struct meta *meta)
{
  struct ape_tag *tag = (struct ape_tag *)meta->data;

  return __ape_tag_save(tag, fp);
}


gchar   *ape_get_property(struct meta *meta, const gchar *property)
{
  struct ape_tag *tag = (struct ape_tag *)meta->data;
  gint len;
  const gchar *value;

  value = ape_tag_read_tag(tag, property, &len);
  return g_strndup(value, len);
}


gboolean ape_set_property(struct meta *meta, const gchar *property, const gchar *value)
{
  struct ape_tag *tag = (struct ape_tag *)meta->data;

  return ape_tag_write_tag(tag, property, value, -1);
}


gboolean ape_release(struct meta *meta)
{
  ape_tag_free((struct ape_tag *)meta->data);

  return TRUE;
}



/************************ TESTING CODE *********************************/

#ifdef TEST_APETAG

int main(int argc, char **argv)
{
  struct ape_tag *tag;
  const gchar *artist;
  const gchar *title;
  gint len;

  tag = ape_tag_load(argv[1]);
  if(! tag)  {
	printf("Failed loading tag.\n");
	return 1;
  }

  artist = ape_tag_read_tag(tag, "artist", &len);
  if(artist)
	printf("extracted artist: %*s\n", len, artist);
  title = ape_tag_read_tag(tag, "title", &len);
  if(title)
	printf("extracted title: %*s\n", len, title);

  dump_ape_tag(tag);

  if(! ape_tag_write_tag(tag, "artist", "Qin Xiaoxue", -1))
	printf("Failed write tag artist");

  dump_ape_tag(tag);

  ape_tag_free(tag);

  return 0;
}

#endif

